/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.filesystems; import java.beans.*; import java.io.*; import java.util.*; import java.util.zip.*; import java.util.jar.*; import org.openide.filesystems.*; import org.openide.filesystems.FileSystem; import org.openide.util.actions.SystemAction; import org.openide.util.enum.EmptyEnumeration; import org.openide.util.NbBundle; import org.openide.util.Task; /** A virtual file system based on a JAR archive. * * @author Jan Jancura, Jaroslav Tulach, Petr Hamernik */ public class JarFileSystem extends AbstractFileSystem implements AbstractFileSystem.List, AbstractFileSystem.Info, AbstractFileSystem.Change, AbstractFileSystem.Attr { /** generated Serialized Version UID */ static final long serialVersionUID = -98124752801761145L; /** File system name prefix */ private static final String JAR_FS = "JAR_FS "; // NOI18N /** a set of all folders in the archive (String, EntryInfo) */ private transient Map allFolders; /** * Opened zip file of this file system is stored here or null. */ private transient JarFile jar; /** Manifest file for jar */ private transient Manifest manifest; /** Archive file. */ private File root = new File ("."); // NOI18N /** Task that scans the content of jar. */ private transient Task scanning; /** Watches modification on root file */ private transient ModifiedWatcher modifiedWatcher; /** False, if ZipEntry last modification date/time reliability has not been yet tested. */ private static boolean entryDatesTested = false; /** True, if ZipEntry date/time support is reliable. False if not. */ private static boolean entryDatesOK = true; /** * Default constructor. */ public JarFileSystem () { this.list = this; this.info = this; this.change = this; this.attr = this; // refreshing setRefreshTime(5000); } /** * Constructor that can provide own capability for the file system. * @param cap the capability */ public JarFileSystem (FileSystemCapability cap) { this (); setCapability (cap); } /** Getter for entry. * @param file filename to scan for * @return entry for given file or null if the info does not exists */ private final EntryInfo getEntryInfo (String file) { return (EntryInfo)getFolders ().get (file); } /** Getter for entry. */ private final JarEntry getEntry (String file) { JarFile j = jar; JarEntry je = j == null ? null : j.getJarEntry (file); if (je == null) { return new JarEntry (file); } else { return je; } } /** Getter for all folders in the file. * @return set of names of all folders */ private final Map getFolders () { if (modifiedWatcher != null) { //System.out.println("Rescnning from getFolders()"); // NOI18N modifiedWatcher.rescanIfNeeded(); } getScanningTask ().waitFinished (); return allFolders; } /** Getter for the scanning task. If not running, * new task is started. */ private Task getScanningTask () { if (scanning == null) { synchronized (this) { if (scanning == null) { rescan (); } } } return scanning; } /** Rescans content of the JAR file. */ private void rescan () { manifest = null; scanning = new Task (new Runnable () { public void run () { HashMap s = new HashMap (23); if (jar == null) { // ok, that is everything allFolders = s; return; } try { Enumeration en = jar.entries (); while (en.hasMoreElements ()) { JarEntry je = (JarEntry)en.nextElement (); String name = je.getName (); addFile (s, name); } } catch (IllegalStateException ex) { // if the jar file is closed } allFolders = s; } }); new Thread (scanning, "Parsing JAR: " + root).start (); // NOI18N } /** Adds a file together with all subdirectories to the hashtable * @param hash hashtable * @param name name of the file to add */ private static void addFile (Map hash, String name) { // if the name ends with slash takes it away if (name.endsWith ("/")) { // NOI18N name = name.substring (0, name.length () - 1); } // work only with slashes name = name.replace ('\\', '/'); while (!"".equals (name)) { // NOI18N int prev = name.lastIndexOf ('/'); String parentName = prev < 0 ? "" : name.substring (0, prev);; // NOI18N // info for parent EntryInfo ei = (EntryInfo)hash.get (parentName); if (ei == null) { // if info is not registered, register new ei = new EntryInfo (); hash.put (parentName, ei); } // add parent ei.addChild (name.substring (prev + 1)); // proceed to parent name = parentName; } } /** Get the JAR manifest. * It will be lazily initialized. * @return parsed manifest file for this archive */ public Manifest getManifest() { if (manifest == null) { try { JarFile j = jar; manifest = j == null ? null : j.getManifest (); } catch (IOException ex) { } if (manifest == null) { manifest = new Manifest (); } } return manifest; } /** * Set name of the ZIP/JAR file. * @param aRoot path to new ZIP or JAR file * @throws IOException if the file is not valid */ public synchronized void setJarFile (File aRoot) throws IOException, PropertyVetoException { // System.out.println("setJarFile to:"+aRoot); // NOI18N if (!aRoot.exists ()) FSException.io ("EXC_FileNotExists", aRoot.toString ()); // NOI18N if (!aRoot.canRead ()) FSException.io ("EXC_CanntRead", aRoot.toString ()); // NOI18N if (!aRoot.isFile ()) FSException.io ("EXC_NotValidFile", aRoot.toString ()); // NOI18N // System.out.println("setJarFile #2"); // NOI18N String s; try { s = aRoot.getCanonicalPath (); } catch (IOException e) { FSException.io ("EXC_NotValidFile", aRoot.toString ()); // NOI18N s = null; } setSystemName (s); // System.out.println("setJarFile, systemName:"+JAR_FS + s); // NOI18N try { jar = new JarFile (s); } catch (ZipException e) { FSException.io ("EXC_NotValidJarFile"); // NOI18N } // System.out.println("setJarFile #3"); // NOI18N root = new File (s); rescan (); // System.out.println("setJarFile #4:"+root); // NOI18N firePropertyChange ("root", null, refreshRoot ()); // NOI18N // watch the modifications of root file if (modifiedWatcher == null) { modifiedWatcher = new ModifiedWatcher(); //new Thread(modifiedWatcher).start(); } } /** Get the file path for the ZIP or JAR file. * @return the file path */ public File getJarFile () { // JST return root; } /* * Provides name of the system that can be presented to the user. * @return user presentable name of the file system */ public String getDisplayName () { if (root == null) return getString ("JAR_NotValidJarFileSystem"); return root.getName(); } /** This file system is read-only. * @return <code>true</code> */ public boolean isReadOnly () { return true; } /** Prepare environment for external compilation or execution. * <P> * Adds name of the ZIP/JAR file, if it has been set, to the class path. */ public void prepareEnvironment (Environment env) { if (root != null) { env.addClassPath (root.toString ()); } } /** Initializes the root of FS. */ private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject (); try { setJarFile (root); } catch (PropertyVetoException ex) { throw new IOException (ex.getMessage ()); } } // // List // private static final String[] EMPTY_ARRAY = {}; /* Scans children for given name */ public String[] children (String name) { EntryInfo ei = getEntryInfo (name); if (ei == null) { return EMPTY_ARRAY; } Collection l = ei.getChildren (); return l == null ? EMPTY_ARRAY : (String[])l.toArray (EMPTY_ARRAY); } // // Change // /* Creates new folder named name. * @param name name of folder * @throws IOException if operation fails */ public void createFolder (String name) throws java.io.IOException { throw new IOException (); } /* Create new data file. * * @param name name of the file * * @return the new data file object * @exception IOException if the file cannot be created (e.g. already exists) */ public void createData (String name) throws IOException { throw new IOException (); } /* Renames a file. * * @param oldName old name of the file * @param newName new name of the file */ public void rename(String oldName, String newName) throws IOException { throw new IOException (); } /* Delete the file. * * @param name name of file * @exception IOException if the file could not be deleted */ public void delete (String name) throws IOException { throw new IOException (); } // // Info // /* * Get last modification time. * @param name the file to test * @return the date */ public java.util.Date lastModified(String name) { if (!entryDatesTested) { JarEntry je = getEntry(name); // JDK 1.3 for WinNT bug test: Jar/ZipEntries have date set to the autumn of 1979. entryDatesOK = new java.util.Date(je.getTime()).getYear() >= 1980; entryDatesTested = true; } if (entryDatesOK) { return new java.util.Date (getEntry (name).getTime ()); } else { return new java.util.Date(this.root.lastModified()); } } /* Test if the file is folder or contains data. * @param name name of the file * @return true if the file is folder, false otherwise */ public boolean folder (String name) { return "".equals (name) || getEntryInfo (name) != null; // NOI18N } /* Test whether this file can be written to or not. * @param name the file to test * @return <CODE>true</CODE> if file is read-only */ public boolean readOnly (String name) { return true; } /** Get the MIME type of the file. * Uses {@link FileUtil#getMIMEType}. * * @param name the file to test * @return the MIME type textual representation, e.g. <code>"text/plain"</code> */ public String mimeType (String name) { int i = name.lastIndexOf ('.'); String s; try { s = FileUtil.getMIMEType (name.substring (i + 1)); } catch (IndexOutOfBoundsException e) { s = null; } return s == null ? "content/unknown" : s; // NOI18N } /* Get the size of the file. * * @param name the file to test * @return the size of the file in bytes or zero if the file does not contain data (does not * exist or is a folder). */ public long size (String name) { return getEntry (name).getSize (); } /* Get input stream. * * @param name the file to test * @return an input stream to read the contents of this file * @exception FileNotFoundException if the file does not exists or is invalid */ public InputStream inputStream (String name) throws java.io.FileNotFoundException { InputStream is = null; try { JarFile j = jar; if (j != null) { JarEntry je = j.getJarEntry (name); if (je != null) { is = j.getInputStream (je); } } } catch (java.io.FileNotFoundException e) { throw e; } catch (IOException e) { throw new java.io.FileNotFoundException (e.getMessage ()); } if (is == null) { throw new java.io.FileNotFoundException (name); } return is; } /* Get output stream. * * @param name the file to test * @return output stream to overwrite the contents of this file * @exception IOException if an error occures (the file is invalid, etc.) */ public OutputStream outputStream (String name) throws java.io.IOException { throw new IOException (); } /* Does nothing. * * @param name name of the file */ public void lock (String name) throws IOException { FSException.io ("EXC_CannotLock", name, getDisplayName (), name); // NOI18N } /* Unlock the file. Does nothing. * * @param name name of the file */ public void unlock (String name) { } /* Does nothing. * * @param name the file to mark */ public void markUnimportant (String name) { } /* Get the file attribute with the specified name. * @param name the file * @param attrName name of the attribute * @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason) */ public Object readAttribute(String name, String attrName) { Attributes attr = getManifest ().getAttributes (name); return attr == null ? null : attr.get (attrName); } /* Set the file attribute with the specified name. * @param name the file * @param attrName name of the attribute * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular file systems may or may not use serialization to store attribute values. * @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}. */ public void writeAttribute(String name, String attrName, Object value) throws IOException { throw new IOException (); } /* Get all file attribute names for the file. * @param name the file * @return enumeration of keys (as strings) */ public Enumeration attributes(String name) { Attributes attr = getManifest ().getAttributes (name); if (attr != null) { return Collections.enumeration (attr.keySet ()); } else { return EmptyEnumeration.EMPTY; } } /* Called when a file is renamed, to appropriatelly update its attributes. * <p> * @param oldName old name of the file * @param newName new name of the file */ public void renameAttributes (String oldName, String newName) { } /* Called when a file is deleted to also delete its attributes. * * @param name name of the file */ public void deleteAttributes (String name) { } /** Close the jar file when we go away...*/ protected void finalize () throws Throwable { super.finalize(); if (jar != null) jar.close(); } /** Info about one entry. Can be tested to be folder and if so the * array of children names can be obtained. */ private static class EntryInfo extends Object { /** vector with children names (String) * @associates String*/ private Collection children; /** Adds new child. * @param name name of child to add */ void addChild (String name) { if (children == null) { children = new HashSet (); } children.add (name); } /** Test if the entry represents a folder (has children) * @return true if so */ public boolean isFolder () { return children != null; } /** Return vector with children names. * @return vector of Strings */ public Collection getChildren () { return children; } } // end of EntryInfo inner class /** Periodically tests the root jar file for modifications * and runs rescanning task if modifications are discovered. */ private final class ModifiedWatcher implements Runnable { /** Date of last modification */ long lastModification = 0; ModifiedWatcher () { synchronized (JarFileSystem.this) { File rootJar = getJarFile(); if (rootJar != null) lastModification = rootJar.lastModified(); } } public synchronized void run () { while (true) { try { wait(10000); } catch (InterruptedException exc) { continue; } rescanIfNeeded(); } } /** Starts rerscanning task if root file modified */ void rescanIfNeeded () { synchronized (JarFileSystem.this) { File rootJar = getJarFile(); if (rootJar == null) return; long curModif = rootJar.lastModified(); if (curModif != lastModification) { lastModification = curModif; JarFileSystem.this.scanning = null; try { //System.out.println("Refreshing from rescanIfNeeded..."); // NOI18N if (jar != null) jar.close(); jar = new JarFile(root); getScanningTask().waitFinished(); } catch (Exception exc) { if (System.getProperty ("netbeans.debug.exceptions") != null) exc.printStackTrace(); } /*JarFileSystem.this.firePropertyChange( "root", JarFileSystem.this.root, refreshRoot ());*/ // NOI18N } } } } // end of ModifiedWatcher /* public static void main (String[] args) throws Exception { JarFileSystem fs = new JarFileSystem (); fs.setJarFile (new File (args[0])); FileObject fo = fs.getRoot (); FileObject[] arr = fo.getChildren (); for (int i = 0; i < arr.length; i++) { System.out.println (" " + arr[i]); } // cycle (fo); // createData (fo, args[1], args[2]); // createFolder (fo, args[1]); // delete (fs.findResource (args[1])); } */ } /* * Log * 21 Gandalf-post-FCS1.19.2.0 3/29/00 Svatopluk Dedic Workaround for JDK's * ZipEntry's bug * 20 src-jtulach1.19 1/13/00 Ian Formanek NOI18N * 19 src-jtulach1.18 1/12/00 Ian Formanek NOI18N * 18 src-jtulach1.17 1/9/00 Jaroslav Tulach #5059 * 17 src-jtulach1.16 12/30/99 Jaroslav Tulach New dialog for * notification of exceptions. * 16 src-jtulach1.15 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 15 src-jtulach1.14 10/7/99 Jaroslav Tulach #4316 * 14 src-jtulach1.13 9/24/99 Jaroslav Tulach #3735 * 13 src-jtulach1.12 7/25/99 Ian Formanek Exceptions printed to * console only on "netbeans.debug.exceptions" flag * 12 src-jtulach1.11 6/10/99 Jaroslav Tulach Capabilities can be * passed to constructor. * 11 src-jtulach1.10 6/10/99 David Simonek closing jar on * finalize.. * 10 src-jtulach1.9 6/10/99 David Simonek refreshing now ok * 9 src-jtulach1.8 6/8/99 Ian Formanek ---- Package Change To * org.openide ---- * 8 src-jtulach1.7 6/8/99 David Simonek bugfixes.... * 7 src-jtulach1.6 5/24/99 Jaroslav Tulach Survives backslashes in * the zip. * 6 src-jtulach1.5 5/14/99 Jaroslav Tulach Serialization works. * 5 src-jtulach1.4 3/26/99 Jesse Glick [JavaDoc] * 4 src-jtulach1.3 3/26/99 Jaroslav Tulach * 3 src-jtulach1.2 3/26/99 Jaroslav Tulach Refresh & Bundles * 2 src-jtulach1.1 3/26/99 Jaroslav Tulach * 1 src-jtulach1.0 3/26/99 Jaroslav Tulach * $ * Beta Change History: * 0 Tuborg 0.13 --/--/98 Jaroslav Tulach fire root change * 0 Tuborg 0.14 --/--/98 Jaroslav Tulach environment support * 0 Tuborg 0.15 --/--/98 Petr Hamernik attributes implementation (manifest file) * 0 Tuborg 0.16 --/--/98 Jaroslav Tulach findResource added * 0 Tuborg 0.20 --/--/98 Jaroslav Tulach speed up * 0 Tuborg 0.21 --/--/98 Jan Jancura system name changed * 0 Tuborg 0.22 --/--/98 Petr Hamernik finding manifest file improved. */